{"componentChunkName":"component---src-templates-blog-post-blog-post-js","path":"/articulos/problema-n1-api-postgresql-typeorm/","result":{"data":{"mdx":{"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"El problema N+1 en las API: por qué tu backend hace demasiadas consultas\",\n  \"seoTitle\": \"Problema N+1 en API: soluciones con PostgreSQL y TypeORM\",\n  \"description\": \"Aprende a detectar y solucionar el problema N+1 en PostgreSQL y TypeORM. Ejemplos con NestJS, JOIN, consultas por lotes y EXPLAIN ANALYZE para mejorar el rendimiento de una API.\",\n  \"description_sort\": \"Detecta N+1 y optimiza tu API con TypeORM, JOIN y consultas por lotes\",\n  \"slug\": \"problema-n1-api-postgresql-typeorm\",\n  \"date\": \"2026-09-08\",\n  \"category\": \"Backend\",\n  \"featuredImage\": \"problema-n1-portada.webp\",\n  \"twitterUsername\": \"@CursosDesarrol1\",\n  \"tags\": [\"postgresql\", \"typeorm\", \"nestjs\", \"backend\"],\n  \"keywords\": [\"problema N+1\", \"N+1 queries\", \"optimizar consultas PostgreSQL\", \"rendimiento API NestJS\", \"TypeORM N+1\", \"consultas SQL lentas\", \"optimización backend\"]\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, mdx(\"span\", {\n    parentName: \"p\",\n    \"className\": \"gatsby-resp-image-wrapper\",\n    \"style\": {\n      \"position\": \"relative\",\n      \"display\": \"block\",\n      \"marginLeft\": \"auto\",\n      \"marginRight\": \"auto\",\n      \"maxWidth\": \"1200px\"\n    }\n  }, \"\\n      \", mdx(\"a\", {\n    parentName: \"span\",\n    \"className\": \"gatsby-resp-image-link\",\n    \"href\": \"/static/1f7e2617b5a61657f37b35ab71d68d3c/ddeab/problema-n1-portada.webp\",\n    \"style\": {\n      \"display\": \"block\"\n    },\n    \"target\": \"_blank\",\n    \"rel\": \"noopener\"\n  }, \"\\n    \", mdx(\"span\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-background-image\",\n    \"style\": {\n      \"paddingBottom\": \"50%\",\n      \"position\": \"relative\",\n      \"bottom\": \"0\",\n      \"left\": \"0\",\n      \"backgroundImage\": \"url('data:image/webp;base64,UklGRloAAABXRUJQVlA4IE4AAACwAwCdASoUAAoAPtFWo0uoJKMhsAgBABoJZwDG9CHhXeDM6ql2YAD+85M3Ytl2j+lTnjcRkDGd1qAKPAsIbkB+6HWrsc0qtm12/DhcAAA=')\",\n      \"backgroundSize\": \"cover\",\n      \"display\": \"block\"\n    }\n  }), \"\\n  \", mdx(\"img\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-image\",\n    \"alt\": \"Consultas individuales entre una API y su base de datos frente a una petición agrupada\",\n    \"title\": \"Consultas individuales entre una API y su base de datos frente a una petición agrupada\",\n    \"src\": \"/static/1f7e2617b5a61657f37b35ab71d68d3c/5bb53/problema-n1-portada.webp\",\n    \"srcSet\": [\"/static/1f7e2617b5a61657f37b35ab71d68d3c/cb58d/problema-n1-portada.webp 300w\", \"/static/1f7e2617b5a61657f37b35ab71d68d3c/f8b1b/problema-n1-portada.webp 600w\", \"/static/1f7e2617b5a61657f37b35ab71d68d3c/5bb53/problema-n1-portada.webp 1200w\", \"/static/1f7e2617b5a61657f37b35ab71d68d3c/ddeab/problema-n1-portada.webp 1774w\"],\n    \"sizes\": \"(max-width: 1200px) 100vw, 1200px\",\n    \"style\": {\n      \"width\": \"100%\",\n      \"height\": \"100%\",\n      \"margin\": \"0\",\n      \"verticalAlign\": \"middle\",\n      \"position\": \"absolute\",\n      \"top\": \"0\",\n      \"left\": \"0\"\n    },\n    \"loading\": \"lazy\"\n  }), \"\\n  \"), \"\\n    \")), mdx(\"h1\", null, \"El problema N+1 en las API: por qu\\xE9 tu backend hace demasiadas consultas\"), mdx(\"p\", null, \"Una API puede funcionar perfectamente durante meses y empezar a responder lentamente cuando aumenta el n\\xFAmero de usuarios. El c\\xF3digo no ha cambiado, los tests siguen pasando y la base de datos no parece estar saturada. Sin embargo, una petici\\xF3n que antes tardaba poco ahora necesita varios segundos.\"), mdx(\"p\", null, \"Una de las causas m\\xE1s habituales de este comportamiento es el \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"problema N+1\"), \".\"), mdx(\"p\", null, \"Se trata de un error de acceso a datos que aparece especialmente en aplicaciones que utilizan ORM como TypeORM, Hibernate, Entity Framework o Sequelize. No es un fallo exclusivo de estas herramientas, pero su abstracci\\xF3n puede hacer que resulte m\\xE1s dif\\xEDcil detectar cu\\xE1ntas consultas SQL est\\xE1 ejecutando realmente la aplicaci\\xF3n.\"), mdx(\"p\", null, \"En este art\\xEDculo veremos c\\xF3mo aparece, c\\xF3mo identificarlo y qu\\xE9 alternativas existen para solucionarlo sin convertir cada consulta en un JOIN gigantesco.\"), mdx(\"h2\", null, \"\\xBFQu\\xE9 es el problema N+1?\"), mdx(\"p\", null, \"El problema N+1 ocurre cuando una aplicaci\\xF3n ejecuta una consulta inicial para obtener una colecci\\xF3n y despu\\xE9s realiza una consulta adicional por cada elemento de esa colecci\\xF3n.\"), mdx(\"p\", null, \"Imaginemos una aplicaci\\xF3n de gesti\\xF3n de proyectos que necesita mostrar una lista de proyectos junto con sus tareas.\"), mdx(\"p\", null, \"La primera consulta obtiene los proyectos:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"SELECT id, name\\nFROM projects\\nORDER BY id\\nLIMIT 100;\"), mdx(\"p\", null, \"Despu\\xE9s, el backend recorre los resultados y obtiene las tareas de cada proyecto:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"SELECT _\\nFROM tasks\\nWHERE project_id = 1;\\n\\nSELECT _\\nFROM tasks\\nWHERE project_id = 2;\\n\\nSELECT \\*\\nFROM tasks\\nWHERE project_id = 3;\\n\\n-- ... hasta el proyecto 100\"), mdx(\"p\", null, \"El resultado es una consulta inicial m\\xE1s cien consultas adicionales.\"), mdx(\"p\", null, \"Es decir:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"1 consulta para obtener proyectos\\n+\\nN consultas para obtener sus tareas\\n=\\nN + 1 consultas\"), mdx(\"p\", null, \"Con diez proyectos puede pasar desapercibido. Con cien, mil o varios usuarios realizando peticiones simult\\xE1neas, el problema empieza a ser significativo.\"), mdx(\"h2\", null, \"Un ejemplo realista con NestJS y TypeORM\"), mdx(\"p\", null, \"Supongamos que tenemos dos entidades relacionadas. Los fragmentos siguientes utilizan la sintaxis de TypeORM 0.3 y omiten los imports de las entidades y la inyecci\\xF3n de repositorios en el servicio NestJS. Declaramos los nombres de tablas y de la clave for\\xE1nea para que coincidan con los ejemplos SQL.\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"@Entity('projects')\\nexport class Project {\\n @PrimaryGeneratedColumn()\\n id: number;\\n\\n @Column()\\n name: string;\\n\\n @OneToMany(() => Task, (task) => task.project)\\n tasks: Task[];\\n}\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"@Entity('tasks')\\nexport class Task {\\n @PrimaryGeneratedColumn()\\n id: number;\\n\\n @Column()\\n title: string;\\n\\n @Column({ name: 'project_id' })\\n projectId: number;\\n\\n @ManyToOne(() => Project, (project) => project.tasks)\\n @JoinColumn({ name: 'project_id' })\\n project: Project;\\n}\"), mdx(\"p\", null, \"La propiedad \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"projectId\"), \" expone la misma columna que utiliza la relaci\\xF3n. Esto nos permitir\\xE1 agrupar tareas sin tener que cargar el objeto \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"project\"), \".\"), mdx(\"p\", null, \"Queremos construir un endpoint que devuelva los proyectos con sus tareas.\"), mdx(\"p\", null, \"Una implementaci\\xF3n aparentemente razonable podr\\xEDa ser:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"async findAll() {\\n const projects = await this.projectRepository.find();\\n\\n return Promise.all(\\n projects.map(async (project) => {\\n const tasks = await this.taskRepository.find({\\n where: {\\n project: { id: project.id },\\n },\\n });\\n\\n return {\\n ...project,\\n tasks,\\n };\\n }),\\n );\\n}\"), mdx(\"p\", null, \"El c\\xF3digo es legible y devuelve exactamente lo que esperamos.\"), mdx(\"p\", null, \"Pero si \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"find()\"), \" devuelve 100 proyectos, ejecutaremos 101 consultas.\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Promise.all()\"), \" no elimina el problema. \\xDAnicamente permite que varias consultas se ejecuten de forma concurrente, dentro de los l\\xEDmites del pool de conexiones y de la base de datos.\"), mdx(\"p\", null, \"De hecho, aumentar la concurrencia puede incrementar la presi\\xF3n sobre PostgreSQL sin reducir el n\\xFAmero total de consultas.\"), mdx(\"h2\", null, \"\\xBFPor qu\\xE9 puede ser tan lento?\"), mdx(\"p\", null, \"Cada consulta tiene un coste.\"), mdx(\"p\", null, \"Aunque PostgreSQL pueda resolver una consulta individual r\\xE1pidamente, el backend debe enviarla, esperar la respuesta, procesar los resultados y gestionar los recursos necesarios.\"), mdx(\"p\", null, \"El tiempo total no depende \\xFAnicamente del trabajo que realiza el motor SQL.\"), mdx(\"p\", null, \"Tambi\\xE9n intervienen:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"la latencia entre la aplicaci\\xF3n y la base de datos;\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"el n\\xFAmero de viajes de ida y vuelta;\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"la planificaci\\xF3n y ejecuci\\xF3n de cada consulta;\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"el pool de conexiones;\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"la cantidad de datos transferidos;\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"la transformaci\\xF3n de resultados en entidades.\")), mdx(\"p\", null, \"Por ejemplo, si una petici\\xF3n necesita realizar cien consultas secuenciales, una peque\\xF1a latencia por consulta puede acumularse. Si se ejecutan concurrentemente, el tiempo puede reducirse, pero el trabajo total y la presi\\xF3n sobre el sistema siguen existiendo.\"), mdx(\"p\", null, \"Por eso no basta con mirar si cada consulta individual es r\\xE1pida.\"), mdx(\"p\", null, \"Tambi\\xE9n debemos observar \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"cu\\xE1ntas consultas ejecuta una petici\\xF3n completa\"), \".\"), mdx(\"h2\", null, \"Soluci\\xF3n 1: cargar las relaciones con un JOIN\"), mdx(\"p\", null, \"La primera alternativa consiste en obtener los proyectos y sus tareas mediante una consulta conjunta.\"), mdx(\"p\", null, \"En TypeORM podemos utilizar QueryBuilder:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"async findAll() {\\n return this.projectRepository\\n .createQueryBuilder('project')\\n .leftJoinAndSelect('project.tasks', 'task')\\n .getMany();\\n}\"), mdx(\"p\", null, \"El ORM genera una consulta SQL equivalente, de forma simplificada, a:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"SELECT\\n project.id,\\n project.name,\\n task.id,\\n task.title,\\n task.project_id\\nFROM projects project\\nLEFT JOIN tasks task\\n ON task.project_id = project.id;\"), mdx(\"p\", null, \"En lugar de realizar una consulta por cada proyecto, obtenemos los datos relacionados en una \\xFAnica operaci\\xF3n SQL.\"), mdx(\"p\", null, \"TypeORM se encarga despu\\xE9s de reconstruir las entidades y sus relaciones.\"), mdx(\"h3\", null, \"\\xBFPor qu\\xE9 LEFT JOIN?\"), mdx(\"p\", null, \"Utilizamos \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"LEFT JOIN\"), \" porque queremos obtener tambi\\xE9n los proyectos que todav\\xEDa no tienen tareas.\"), mdx(\"p\", null, \"Si utiliz\\xE1semos un \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"INNER JOIN\"), \", los proyectos sin tareas quedar\\xEDan fuera del resultado.\"), mdx(\"p\", null, \"Esta distinci\\xF3n es importante: optimizar una consulta no debe cambiar accidentalmente el comportamiento funcional del endpoint.\"), mdx(\"h2\", null, \"Soluci\\xF3n 2: consultas por lotes\"), mdx(\"p\", null, \"Un JOIN no siempre es la mejor soluci\\xF3n.\"), mdx(\"p\", null, \"Imaginemos que necesitamos obtener cien proyectos, pero cada uno puede tener miles de tareas. Una consulta conjunta podr\\xEDa devolver una cantidad enorme de filas y repetir los datos del proyecto muchas veces.\"), mdx(\"p\", null, \"En ese caso podemos utilizar una estrategia de carga por lotes.\"), mdx(\"p\", null, \"Primero obtenemos los proyectos:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"const projects = await this.projectRepository.find({\\n order: { id: 'ASC' },\\n take: 100,\\n});\\n\\nif (projects.length === 0) {\\n return [];\\n}\"), mdx(\"p\", null, \"El orden hace que el l\\xEDmite sea determinista. Si no hay proyectos, devolvemos una lista vac\\xEDa sin consultar tareas.\"), mdx(\"p\", null, \"Despu\\xE9s extraemos sus identificadores:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"const projectIds = projects.map((project) => project.id);\"), mdx(\"p\", null, \"Y finalmente obtenemos todas las tareas necesarias en una \\xFAnica consulta, utilizando \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"In\"), \" y la columna \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"projectId\"), \" que hemos declarado en la entidad:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"import { In } from 'typeorm';\\n\\n// Dentro del mismo método del servicio:\\nconst tasks = await this.taskRepository.find({\\n where: {\\n projectId: In(projectIds),\\n },\\n});\"), mdx(\"p\", null, \"La consulta SQL ser\\xE1 conceptualmente similar a:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"SELECT \\*\\nFROM tasks\\nWHERE project_id IN (1, 2, 3, 4, 5);\"), mdx(\"p\", null, \"Ahora tenemos dos consultas en lugar de N+1.\"), mdx(\"p\", null, \"Podemos agrupar las tareas por proyecto:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"const tasksByProject = new Map<number, Task[]>();\\n\\nfor (const task of tasks) {\\n const projectId = task.projectId;\\n\\n const current = tasksByProject.get(projectId) ?? [];\\n current.push(task);\\n\\n tasksByProject.set(projectId, current);\\n}\"), mdx(\"p\", null, \"Y construir la respuesta:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"return projects.map((project) => ({\\n ...project,\\n tasks: tasksByProject.get(project.id) ?? [],\\n}));\"), mdx(\"p\", null, \"En este ejemplo, \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"task.projectId\"), \" est\\xE1 disponible sin cargar la relaci\\xF3n. Acceder a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"task.project.id\"), \" no ser\\xEDa correcto si \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"project\"), \" no se ha cargado.\"), mdx(\"p\", null, \"La idea importante es que el n\\xFAmero de consultas deja de crecer linealmente con el n\\xFAmero de proyectos. Para listas de identificadores muy grandes conviene dividir la carga en lotes acotados; en ese caso habr\\xE1 una consulta adicional por lote.\"), mdx(\"p\", null, \"Los lotes evitan repetir los datos del proyecto, pero siguen cargando todas las tareas seleccionadas. Si son demasiadas, necesitaremos paginar las tareas o reducir los campos de la respuesta.\"), mdx(\"h2\", null, \"\\xBFJOIN o consultas por lotes?\"), mdx(\"p\", null, mdx(\"span\", {\n    parentName: \"p\",\n    \"className\": \"gatsby-resp-image-wrapper\",\n    \"style\": {\n      \"position\": \"relative\",\n      \"display\": \"block\",\n      \"marginLeft\": \"auto\",\n      \"marginRight\": \"auto\",\n      \"maxWidth\": \"1200px\"\n    }\n  }, \"\\n      \", mdx(\"a\", {\n    parentName: \"span\",\n    \"className\": \"gatsby-resp-image-link\",\n    \"href\": \"/static/1b18560fbc63c503311395683456a5db/ddeab/n1-join-lotes.webp\",\n    \"style\": {\n      \"display\": \"block\"\n    },\n    \"target\": \"_blank\",\n    \"rel\": \"noopener\"\n  }, \"\\n    \", mdx(\"span\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-background-image\",\n    \"style\": {\n      \"paddingBottom\": \"50%\",\n      \"position\": \"relative\",\n      \"bottom\": \"0\",\n      \"left\": \"0\",\n      \"backgroundImage\": \"url('data:image/webp;base64,UklGRmoAAABXRUJQVlA4IF4AAAAQBACdASoUAAoAPtFUo0uoJKMhsAgBABoJZwC7AB7HfVQSmLM72TkyQAD+8DshfBWDarxE6QQxhcyqyn9hBO0cx5rB1uV9/yfXi56aAtQkY7DQGMc8mpR9dOdcwAAA')\",\n      \"backgroundSize\": \"cover\",\n      \"display\": \"block\"\n    }\n  }), \"\\n  \", mdx(\"img\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-image\",\n    \"alt\": \"Comparación para 100 proyectos: N+1 ejecuta 101 consultas, JOIN una consulta y la carga por lotes dos consultas\",\n    \"title\": \"Comparación para 100 proyectos: N+1 ejecuta 101 consultas, JOIN una consulta y la carga por lotes dos consultas\",\n    \"src\": \"/static/1b18560fbc63c503311395683456a5db/5bb53/n1-join-lotes.webp\",\n    \"srcSet\": [\"/static/1b18560fbc63c503311395683456a5db/cb58d/n1-join-lotes.webp 300w\", \"/static/1b18560fbc63c503311395683456a5db/f8b1b/n1-join-lotes.webp 600w\", \"/static/1b18560fbc63c503311395683456a5db/5bb53/n1-join-lotes.webp 1200w\", \"/static/1b18560fbc63c503311395683456a5db/ddeab/n1-join-lotes.webp 1774w\"],\n    \"sizes\": \"(max-width: 1200px) 100vw, 1200px\",\n    \"style\": {\n      \"width\": \"100%\",\n      \"height\": \"100%\",\n      \"margin\": \"0\",\n      \"verticalAlign\": \"middle\",\n      \"position\": \"absolute\",\n      \"top\": \"0\",\n      \"left\": \"0\"\n    },\n    \"loading\": \"lazy\"\n  }), \"\\n  \"), \"\\n    \")), mdx(\"p\", null, mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Recuento ilustrativo para los ejemplos, sin relaciones adicionales ni consultas extra de paginaci\\xF3n. Menos consultas no implica por s\\xED solo un menor tiempo de respuesta.\")), mdx(\"p\", null, \"No existe una respuesta universal.\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Estrategia\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Ventaja principal\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Riesgo\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"JOIN\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Reduce viajes a la base de datos\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Puede multiplicar filas y transferir demasiados datos\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Consultas por lotes\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Controla mejor la carga de relaciones\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Requiere agrupar resultados y gestionar lotes\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Carga individual\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Sencilla para casos aislados\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Produce N+1 cuando se utiliza dentro de colecciones\")))), mdx(\"p\", null, \"Si necesitamos un proyecto concreto con sus tareas, una carga individual puede ser perfectamente adecuada.\"), mdx(\"p\", null, \"Si necesitamos cien proyectos con sus tareas, debemos plantearnos otra estrategia.\"), mdx(\"p\", null, \"Y si cada proyecto tiene decenas de miles de tareas, probablemente el problema no sea \\xFAnicamente c\\xF3mo cargar la relaci\\xF3n, sino si realmente necesitamos devolver todas esas tareas en una misma respuesta.\"), mdx(\"h2\", null, \"El error de cargar todas las relaciones por defecto\"), mdx(\"p\", null, \"Una reacci\\xF3n habitual al descubrir N+1 es configurar todas las relaciones como eager.\"), mdx(\"p\", null, \"Por ejemplo:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"@OneToMany(() => Task, (task) => task.project, {\\n eager: true,\\n})\\ntasks: Task[];\"), mdx(\"p\", null, \"Esto puede evitar consultas adicionales en determinados flujos, pero introduce otro problema: cargar datos que no necesitamos.\"), mdx(\"p\", null, \"Quiz\\xE1 un endpoint solamente necesita devolver:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"{\\n \\\"id\\\": 12,\\n \\\"name\\\": \\\"Proyecto web\\\"\\n}\"), mdx(\"p\", null, \"Si cargamos autom\\xE1ticamente todas las tareas, estaremos consumiendo memoria, tiempo de consulta y ancho de banda sin aportar nada al cliente.\"), mdx(\"p\", null, \"La soluci\\xF3n no consiste en cargar siempre todas las relaciones.\"), mdx(\"p\", null, \"Consiste en \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"definir qu\\xE9 datos necesita cada caso de uso\"), \".\"), mdx(\"h2\", null, \"Dise\\xF1ar DTO de respuesta ayuda a evitar el problema\"), mdx(\"p\", null, \"Una buena pr\\xE1ctica es separar las entidades de base de datos de los DTO que devuelve la API.\"), mdx(\"p\", null, \"Por ejemplo, un listado puede necesitar:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"export class ProjectListDto {\\n id: number;\\n name: string;\\n taskCount: number;\\n}\"), mdx(\"p\", null, \"Mientras que el detalle puede necesitar:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"export class ProjectDetailDto {\\n id: number;\\n name: string;\\n tasks: TaskDto[];\\n}\"), mdx(\"p\", null, \"Son necesidades diferentes y, por tanto, pueden utilizar consultas diferentes.\"), mdx(\"p\", null, \"Para el listado no necesitamos cargar todas las tareas solamente para contarlas.\"), mdx(\"p\", null, \"Podemos realizar una consulta agregada:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"SELECT\\n p.id,\\n p.name,\\n COUNT(t.id) AS task_count\\nFROM projects p\\nLEFT JOIN tasks t\\n ON t.project_id = p.id\\nGROUP BY p.id, p.name;\"), mdx(\"p\", null, \"Esto permite devolver el n\\xFAmero de tareas sin transferir cada tarea al backend.\"), mdx(\"p\", null, \"El dise\\xF1o de la API y el dise\\xF1o de las consultas est\\xE1n directamente relacionados.\"), mdx(\"h2\", null, \"C\\xF3mo detectar N+1 antes de que llegue a producci\\xF3n\"), mdx(\"p\", null, \"El primer paso es activar el registro de consultas durante el desarrollo.\"), mdx(\"p\", null, \"En TypeORM podemos configurar el logging:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"TypeOrmModule.forRoot({\\n type: 'postgres',\\n // ...resto de configuración\\n logging: ['query', 'error'],\\n});\"), mdx(\"p\", null, \"No es recomendable dejar un logging exhaustivo de todas las consultas en producci\\xF3n sin valorar su volumen, coste y posible exposici\\xF3n de datos sensibles.\"), mdx(\"p\", null, \"Durante el desarrollo, sin embargo, permite observar qu\\xE9 ocurre cuando llamamos a un endpoint.\"), mdx(\"p\", null, \"Si vemos algo parecido a esto:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"SELECT ... FROM projects\\n\\nSELECT ... FROM tasks WHERE project_id = 1\\nSELECT ... FROM tasks WHERE project_id = 2\\nSELECT ... FROM tasks WHERE project_id = 3\\nSELECT ... FROM tasks WHERE project_id = 4\"), mdx(\"p\", null, \"tenemos una se\\xF1al clara de que debemos investigar.\"), mdx(\"p\", null, \"Otra t\\xE9cnica \\xFAtil consiste en crear tests de integraci\\xF3n que verifiquen el n\\xFAmero de consultas ejecutadas por determinados casos de uso.\"), mdx(\"p\", null, \"No todos los endpoints necesitan un l\\xEDmite estricto, pero puede ser especialmente \\xFAtil para listados cr\\xEDticos.\"), mdx(\"h2\", null, \"EXPLAIN ANALYZE: comprobar qu\\xE9 est\\xE1 haciendo PostgreSQL\"), mdx(\"p\", null, \"Reducir el n\\xFAmero de consultas es importante, pero no garantiza que la consulta resultante sea eficiente.\"), mdx(\"p\", null, \"Una consulta \\xFAnica tambi\\xE9n puede ser lenta.\"), mdx(\"p\", null, \"PostgreSQL proporciona \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"EXPLAIN ANALYZE\"), \" para estudiar c\\xF3mo ejecuta una consulta:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"EXPLAIN (ANALYZE, BUFFERS)\\nSELECT\\n p.id,\\n p.name,\\n t.id,\\n t.title\\nFROM projects p\\nLEFT JOIN tasks t\\n ON t.project_id = p.id;\"), mdx(\"p\", null, \"El resultado permite analizar aspectos como:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"el plan de ejecuci\\xF3n;\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"el tiempo real empleado;\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"el n\\xFAmero de filas procesadas;\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"los accesos a buffers;\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"los tipos de JOIN utilizados;\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"las diferencias entre filas estimadas y reales.\")), mdx(\"p\", null, \"Es importante recordar que \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ANALYZE\"), \" ejecuta realmente la consulta. Por tanto, hay que utilizarlo con precauci\\xF3n sobre operaciones que modifican datos.\"), mdx(\"p\", null, \"En PostgreSQL 18, \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"EXPLAIN ANALYZE\"), \" muestra la informaci\\xF3n de buffers por defecto. La opci\\xF3n expl\\xEDcita \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"BUFFERS\"), \" del ejemplo tambi\\xE9n permite solicitarla en versiones anteriores. Puedes consultar este cambio en las \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.postgresql.org/docs/18/release-18.html\"\n  }, \"notas de PostgreSQL 18\"), \".\"), mdx(\"h2\", null, \"\\xBFY los \\xEDndices?\"), mdx(\"p\", null, \"Los \\xEDndices pueden mejorar considerablemente determinadas consultas, pero no solucionan por s\\xED solos el problema N+1.\"), mdx(\"p\", null, \"Si ejecutamos cien consultas, a\\xF1adir un \\xEDndice puede hacer que cada una sea m\\xE1s r\\xE1pida.\"), mdx(\"p\", null, \"Seguiremos ejecutando cien consultas.\"), mdx(\"p\", null, \"En el ejemplo anterior, un \\xEDndice sobre la clave utilizada para buscar tareas puede ser \\xFAtil:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"CREATE INDEX idx_tasks_project_id\\nON tasks (project_id);\"), mdx(\"p\", null, \"Pero la decisi\\xF3n debe basarse en el patr\\xF3n real de consultas y en el plan de ejecuci\\xF3n.\"), mdx(\"p\", null, \"Los \\xEDndices tienen costes de almacenamiento y mantenimiento, especialmente durante las escrituras. No conviene crearlos indiscriminadamente.\"), mdx(\"h2\", null, \"PostgreSQL 18 es m\\xE1s r\\xE1pido, pero no arregla una mala estrategia de acceso\"), mdx(\"p\", null, \"PostgreSQL 18, publicado el 25 de septiembre de 2025, introdujo un nuevo subsistema de entrada y salida as\\xEDncrona, mejoras en el planificador y capacidades adicionales de observabilidad, descritos en sus \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.postgresql.org/docs/18/release-18.html\"\n  }, \"notas de la versi\\xF3n\"), \".\"), mdx(\"p\", null, \"Son avances relevantes para el rendimiento de la base de datos.\"), mdx(\"p\", null, \"Sin embargo, una mejora del motor no elimina los errores de dise\\xF1o de la aplicaci\\xF3n.\"), mdx(\"p\", null, \"Si un endpoint realiza 501 consultas para devolver 500 registros, actualizar PostgreSQL no transforma autom\\xE1ticamente ese patr\\xF3n en una consulta eficiente.\"), mdx(\"p\", null, \"El rendimiento de una aplicaci\\xF3n depende de varias capas:\"), mdx(CodeJs, {\n    mdxType: \"CodeJs\"\n  }, \"Cliente\\n ↓\\nAPI\\n ↓\\nLógica de negocio\\n ↓\\nORM / acceso a datos\\n ↓\\nPostgreSQL\\n ↓\\nAlmacenamiento\"), mdx(\"p\", null, \"Optimizar solamente una de ellas puede no resolver el cuello de botella real.\"), mdx(\"h2\", null, \"Una metodolog\\xEDa pr\\xE1ctica para optimizar un endpoint\"), mdx(\"p\", null, \"Cuando una API empieza a responder lentamente, conviene evitar las optimizaciones basadas \\xFAnicamente en intuiciones.\"), mdx(\"p\", null, \"Un procedimiento razonable ser\\xEDa:\"), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Medir el tiempo total del endpoint.\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Registrar cu\\xE1ntas consultas SQL ejecuta.\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Identificar consultas repetidas o innecesarias.\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Comprobar qu\\xE9 datos necesita realmente el cliente.\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Sustituir N+1 por JOIN, lotes o consultas agregadas cuando corresponda.\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Analizar las consultas costosas con EXPLAIN ANALYZE.\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Revisar \\xEDndices y volumen de datos.\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Volver a medir con un conjunto de datos representativo.\")), mdx(\"p\", null, \"La \\xFAltima parte es fundamental.\"), mdx(\"p\", null, \"Una optimizaci\\xF3n no est\\xE1 demostrada porque el c\\xF3digo parezca mejor. Est\\xE1 demostrada cuando las mediciones muestran una mejora sin alterar el comportamiento esperado.\"), mdx(\"h2\", null, \"Conclusi\\xF3n\"), mdx(\"p\", null, \"El problema N+1 es uno de los ejemplos m\\xE1s claros de c\\xF3mo una aplicaci\\xF3n puede tener c\\xF3digo correcto y, al mismo tiempo, una estrategia de acceso a datos deficiente.\"), mdx(\"p\", null, \"Los ORM facilitan enormemente el desarrollo, pero no eliminan la necesidad de comprender SQL.\"), mdx(\"p\", null, \"Saber cu\\xE1ndo utilizar un JOIN, cu\\xE1ndo cargar relaciones por lotes, cu\\xE1ndo realizar una consulta agregada y c\\xF3mo analizar un plan de ejecuci\\xF3n sigue siendo una habilidad esencial para cualquier desarrollador backend.\"), mdx(\"p\", null, \"La lecci\\xF3n no es que debamos abandonar TypeORM ni escribir todo el SQL manualmente.\"), mdx(\"p\", null, \"Es que debemos dejar de tratar la base de datos como una caja negra.\"), mdx(\"p\", null, \"Porque una API r\\xE1pida no depende \\xFAnicamente de tener un servidor potente o una versi\\xF3n reciente de PostgreSQL.\"), mdx(\"p\", null, \"Depende, sobre todo, de pedirle a la base de datos exactamente lo que necesitamos, de la forma adecuada.\"), mdx(\"h2\", null, \"Fuentes y documentaci\\xF3n\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", {\n    parentName: \"li\",\n    \"href\": \"https://typeorm.io/docs/performance-optimization/introduction/\"\n  }, \"TypeORM: Performance and optimization\")), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", {\n    parentName: \"li\",\n    \"href\": \"https://typeorm.io/docs/performance-optimization/efficient-use-of-query-builder/\"\n  }, \"TypeORM: Efficient use of Query Builder\")), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", {\n    parentName: \"li\",\n    \"href\": \"https://www.postgresql.org/docs/18/release-18.html\"\n  }, \"PostgreSQL 18: notas de la versi\\xF3n\")), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", {\n    parentName: \"li\",\n    \"href\": \"https://www.postgresql.org/docs/18/using-explain.html\"\n  }, \"PostgreSQL: documentaci\\xF3n de EXPLAIN\"))));\n}\n;\nMDXContent.isMDXComponent = true;","frontmatter":{"twitterUsername":"@CursosDesarrol1","title":"El problema N+1 en las API: por qué tu backend hace demasiadas consultas","seoTitle":"Problema N+1 en API: soluciones con PostgreSQL y TypeORM","date":"2026-09-08","description":"Aprende a detectar y solucionar el problema N+1 en PostgreSQL y TypeORM. Ejemplos con NestJS, JOIN, consultas por lotes y EXPLAIN ANALYZE para mejorar el rendimiento de una API.","featuredImage":{"childImageSharp":{"fluid":{"base64":"data:image/webp;base64,UklGRloAAABXRUJQVlA4IE4AAACwAwCdASoUAAoAPtFWo0uoJKMhsAgBABoJZwDG9CHhXeDM6ql2YAD+85M3Ytl2j+lTnjcRkDGd1qAKPAsIbkB+6HWrsc0qtm12/DhcAAA=","aspectRatio":2,"src":"/static/1f7e2617b5a61657f37b35ab71d68d3c/ccdb5/problema-n1-portada.webp","srcSet":"/static/1f7e2617b5a61657f37b35ab71d68d3c/6b183/problema-n1-portada.webp 200w,\n/static/1f7e2617b5a61657f37b35ab71d68d3c/fc32b/problema-n1-portada.webp 400w,\n/static/1f7e2617b5a61657f37b35ab71d68d3c/ccdb5/problema-n1-portada.webp 800w,\n/static/1f7e2617b5a61657f37b35ab71d68d3c/9000d/problema-n1-portada.webp 1200w,\n/static/1f7e2617b5a61657f37b35ab71d68d3c/e0075/problema-n1-portada.webp 1600w,\n/static/1f7e2617b5a61657f37b35ab71d68d3c/8337b/problema-n1-portada.webp 1774w","sizes":"(max-width: 800px) 100vw, 800px"}}}}}},"pageContext":{"slug":"/articulos/problema-n1-api-postgresql-typeorm/","previous":{"fields":{"slug":"/avisos_legales/politica_cookies/"}},"next":null}},"staticQueryHashes":["2032873950"]}